
/*
 * Inventory.d: Stuff related to the inventory and items.
 * Author: Guilherme R. Lampert
 * 2013-11-25
 */

import std.stdio;
import std.random;
import GLStuff;

// ======================================
// Local helpers:
// ======================================

// Hardcoded slot size: 32x32 pixels
enum PixelsPerInventorySlot = 32;

// Offset X, when drawing 2 inventories side by side.
private int drawOffset = 0;

class ItemSymbol {

	// When not drawing a sprite, just a char in the console:
	char charSymbol;

	// When using a GL image:
	Image imgSymbol;

	// Default constructor:
	this(char charSymbol, Image imgSymbol)
	{
		this.charSymbol = charSymbol;
		this.imgSymbol  = imgSymbol;
	}

	// Draw this symbol to screen or console:
	void Draw(int x, int y, int w, int h)
	{
		if (noGL)
		{
			write(charSymbol);
		}
		else
		{
			assert(imgSymbol !is null);
			assert(w == (imgSymbol.width  / PixelsPerInventorySlot));
			assert(h == (imgSymbol.height / PixelsPerInventorySlot));
			DrawImage((x * PixelsPerInventorySlot) + drawOffset, y * PixelsPerInventorySlot, imgSymbol.width, imgSymbol.height, imgSymbol);
		}
	}
}

// Item sprites, used when doing 2D drawing with GL:
private Image[] defaultItems = null;
private void InitItemSprites()
{
	defaultItems = new Image[7];
	defaultItems[0] = LoadImageFromFile("Sprites/belt_2x1.tga");
	defaultItems[1] = LoadImageFromFile("Sprites/claw_1x2.tga");
	defaultItems[2] = LoadImageFromFile("Sprites/helm_2x2.tga");
	defaultItems[3] = LoadImageFromFile("Sprites/potion_1x1.tga");
	defaultItems[4] = LoadImageFromFile("Sprites/shield_2x4.tga");
	defaultItems[5] = LoadImageFromFile("Sprites/sword_1x3.tga");
	defaultItems[6] = LoadImageFromFile("Sprites/sword_2x3.tga");
}

// Get the size of one of the available random items, in inventory slots:
private void RandomItemSize(out int w, out int h, int maxItemSize)
{
	if (noGL)
	{
		// Any random value between 1 and maxItemSize:
		w = uniform(1, maxItemSize + 1);
		h = uniform(1, maxItemSize + 1);
	}
	else // One of the available sizes:
	{
		if (defaultItems is null)
		{
			InitItemSprites();
		}

		assert(defaultItems.length > 0);
		const int randIndex = uniform(0, cast(int)defaultItems.length);
		w = defaultItems[randIndex].width  / PixelsPerInventorySlot;
		h = defaultItems[randIndex].height / PixelsPerInventorySlot;
	}
}

// Get a random symbol for an item, best matching the item size.
private ItemSymbol RandomItemSymbol(int w, int h)
{
	if (noGL)
	{
		return new ItemSymbol(cast(char)uniform(cast(int)'a', cast(int)'z'), null);
	}
	else
	{
		// w and y are only used when drawing item sprites.
		// Use them to find a sprite of the given size for the item.
		if (defaultItems is null)
		{
			InitItemSprites();
		}

		foreach (ref Image img; defaultItems)
		{
			if (((img.width / PixelsPerInventorySlot) == w) && ((img.height / PixelsPerInventorySlot) == h))
			{
				return (new ItemSymbol('\0', img));
			}
		}

		assert(0);
	}
}

// Draw the empty inventory slot icon to the scree:
private Image emptySlotImg = null;
private void DrawEmptySlot(int x, int y)
{
	if (emptySlotImg is null)
	{
		emptySlotImg = LoadImageFromFile("Sprites/empty_slot.tga");
		assert(emptySlotImg !is null);
	}

	DrawImage((x * emptySlotImg.width) + drawOffset, y * emptySlotImg.height, emptySlotImg.width, emptySlotImg.height, emptySlotImg);
}

// ======================================
// Item:
// ======================================

class Item {

	// Item rectangle and area:
	int x;
	int y;
	int width;
	int height;
	int area; // => width * height:

	// Used for drawing only.
	ItemSymbol symbol;

	// Construct default:
	this()
	{
		this.x      = -1;
		this.y      = -1;
		this.width  = -1;
		this.height = -1;
		this.area   = -1;
	}

	// Construct sized rectangle:
	this(int x, int y, int width, int height, ItemSymbol symbol)
	{
		assert(x >= 0);
		assert(y >= 0);
		assert(width  >= 0);
		assert(height >= 0);
		assert(symbol !is null);

		this.x      = x;
		this.y      = y;
		this.width  = width;
		this.height = height;
		this.area   = (width * height);
		this.symbol = symbol;
	}

	// Test if this item is valid (has a non negative position assigned to it).
	bool IsValid()
	{
		return (x >= 0 && y >= 0);
	}

	// Test if x,y is a point inside this rectangle:
	bool IsPointInside(int x, int y)
	{
		return !((x < this.x) || (y < this.y) || (x >= (this.x + this.width)) || (y >= (this.y + this.height)));
	}

	// Test if a rectangle intersects this item's rectangle:
	bool RectIntersect(int x, int y, int w, int h)
	{
		return (((this.y + this.height) > y) && (this.y < (y + h)) && ((this.x + this.width) > x) && (this.x < (x + w)));
	}

	// Check if items are neighbors:
	bool IsNextTo(in Item item)
	{
		if ((item.x == (this.x + this.width)) && ((item.y + item.height) <= (this.y + this.height)))
		{
			return (true);
		}
		if ((item.y == (this.y + this.height)) && ((item.x + item.width) <= (this.x + this.width)))
		{
			return (true);
		}
		return (false);
	}

	// Draws the item:
	void DrawItem()
	{
		symbol.Draw(x, y, width, height);
	}

	// Comparison operator for sorting:
	int opCmp(ref const Item other) const
	{
		if (this.area > other.area)
		{
			return (-1); // Greater area goes first
		}
		if (this.area < other.area)
		{
			return (+1); // Smaller area to the end
		}
		return (0); // Equal
	}
}

// ======================================
// Inventory:
// ======================================

class Inventory {

	// Dimensions and area of inventory:
	immutable int width;
	immutable int height;
	immutable int area;

	// Inventory matrix, with indexes to the items:
	int[] slots;

	// List of items:
	Item[] items;

	// Construct with item list:
	this(int width, int height, Item[] items)
	{
		this.width  = width;
		this.height = height;
		this.area   = (width * height);
		this.slots  = new int[this.area];
		this.items  = items;

		// -1 is an empty slot:
		for (int i = 0; i < this.area; ++i)
		{
			this.slots[i] = -1;
		}

		// The rest get an item index:
		for (int i = 0; i < this.items.length; ++i)
		{
			for (int y = items[i].y; y < (items[i].y + items[i].height); ++y)
			{
				for (int x = items[i].x; x < (items[i].x + items[i].width); ++x)
				{
					const int slotIndex = (x + y * this.width);
					if (slotIndex < slots.length && slots[slotIndex] == -1)
					{
						slots[slotIndex] = i;
					}
				}
			}
		}
	}

	// Construct with params:
	this(int width, int height, int numItems)
	{
		this.width  = width;
		this.height = height;
		this.area   = (width * height);
		this.slots  = new int[this.area];
		this.items  = new Item[numItems];

		// -1 is an empty slot:
		for (int i = 0; i < this.area; ++i)
		{
			this.slots[i] = -1;
		}

		// Init item array:
		for (int i = 0; i < numItems; ++i)
		{
			this.items[i] = new Item();
		}
	}

	// Grab a reference to an item at a given slot:
	Item ItemAt(int x, int y)
	{
		for (int i = 0; i < items.length; ++i)
		{
			if (items[i].IsPointInside(x, y))
			{
				return (items[i]);
			}
		}
		return new Item(); // Null item
	}

	// Test if the given x,y slot inside the inventory space is occupied by some item:
	bool IsOccupied(int x, int y)
	{
		for (int i = 0; i < items.length; ++i)
		{
			if (items[i].IsValid() && items[i].IsPointInside(x, y))
			{
				return (true);
			}
		}
		return (false);
	}

	// Test if a rectangle of slots inside the inventory space is occupied by some item:
	bool IsOccupied(int x, int y, int w, int h)
	{
		for (int i = 0; i < items.length; ++i)
		{
			if (items[i].IsValid() && items[i].RectIntersect(x, y, w, h))
			{
				return (true);
			}
		}
		return (false);
	}

	// Place an item in the first free index:
	bool PlaceItem(Item item)
	{
		for (int i = 0; i < items.length; ++i)
		{
			if (!items[i].IsValid())
			{
				for (int y = item.y; y < (item.y + item.height); ++y)
				{
					for (int x = item.x; x < (item.x + item.width); ++x)
					{
						const int slotIndex = (x + y * this.width);
						if (slotIndex < slots.length && slots[slotIndex] == -1)
						{
							slots[slotIndex] = i;
						}
					}
				}

				items[i] = item;
				return (true);
			}
		}
		return (false);
	}

	// Place larger items first:
	void PlaceItemsLargerFirst(ref Item[] itemList)
	{
		// Sort putting larger items at the front of the list (see Item.opCmp):
		std.algorithm.sort(itemList);

		int itemsPlaced = 0, i = 0;
		for (; itemsPlaced < itemList.length; ++i)
		{
			if (i == itemList.length)
			{
				i = 0;
			}

			if (!itemList[i].IsValid())
			{
				++itemsPlaced;
				continue;
			}

			if (CheckedPlaceItem(itemList[i]))
			{
				++itemsPlaced;
			}
		}
	}

	// Helper used by PlaceItemsLargerFirst():
	private bool CheckedPlaceItem(Item item)
	{
		for (int y = 0; y < this.height; ++y)
		{
			for (int x = 0; x < this.width; ++x)
			{
				if (slots[x + y * this.width] == -1)
				{
					int numEmpty = 0;

					// See if there are enough empty slots in the neighborhood to fit the item:
					for (int j = y; j < (y + item.height); ++j)
					{
						for (int k = x; k < (x + item.width); ++k)
						{
							const int slotIndex = (k + j * this.width);
							if (slotIndex < slots.length && slots[slotIndex] == -1)
							{
								++numEmpty;
							}
						}
					}

					if (numEmpty == item.area)
					{
						PlaceItem(new Item(x, y, item.width, item.height, item.symbol));
						return (true);
					}
				}
			}
		}

		return (false);
	}

	// Check if there are items overlapping each other or going outside the inventory bounds.
	bool HasMisplacedItems()
	{
		// Check if any item goes outside the inventory area:
		for (int i = 0; i < items.length; ++i)
		{
			if ((items[i].x < 0) || (items[i].y < 0))
			{
				return (true);
			}
			if (((items[i].x + items[i].width)  > this.width) ||
				((items[i].y + items[i].height) > this.height))
			{
				return (true);
			}
		}

		// Test each item against every other item to test for overlapping bounds:
		for (int a = 0; a < items.length; ++a)
		{
			if (items[a].IsValid())
			{
				for (int b = 0; b < items.length; ++b)
				{
					if (items[b].IsValid() && (b != a))
					{
						if (items[a].RectIntersect(items[b].x, items[b].y, items[b].width, items[b].height))
						{
							return (true); // a and b overlapped!
						}
					}
				}
			}
		}

		return (false); // No overlapping or out-of-bounds item found.
	}

	// Compute a rate for this inventory, based on how its items are placed.
	double EvaluateItemPlacement()
	{
		// Rate is computed based on:
		// 1) Size of bounding box that can accommodate all items (smaller better);
		// 2) Number of empty gaps between items inside the bounding box (smaller better);
		// 3) Position of bounding box. Lower x and y are better rated.

		// Compute bounding box:
		int mins[2] = [int.max, int.max];
		int maxs[2] = [int.min, int.min];
		bool oneValid = false;

		for (int i = 0; i < items.length; ++i)
		{
			if (!items[i].IsValid())
			{
				continue;
			}

			if (items[i].x < mins[0]) { mins[0] = items[i].x; }
			if (items[i].y < mins[1]) { mins[1] = items[i].y; }

			if ((items[i].x + items[i].width)  > maxs[0]) { maxs[0] = (items[i].x + items[i].width);  }
			if ((items[i].y + items[i].height) > maxs[1]) { maxs[1] = (items[i].y + items[i].height); }

			oneValid = true;
		}

		if (!oneValid)
		{
			return (0.0);
		}

		if (mins[0] < 0) { mins[0] = 0; }
		if (mins[1] < 0) { mins[1] = 0; }
		if (maxs[0] > this.width)  { maxs[0] = this.width;  }
		if (maxs[1] > this.height) { maxs[1] = this.height; }

		// Count gaps of empty space inside bb:
		int gaps = 0;
		for (int y = mins[1]; y < maxs[1]; ++y)
		{
			for (int x = mins[0]; x < maxs[0]; ++x)
			{
				const int slotIndex = (x + y * this.width);
				if (slotIndex < slots.length)
				{
					if (slots[slotIndex] == -1)
					{
						++gaps;
					}
				}
			}
		}

		// Compute rating:
		const double bbArea = ((maxs[0] - mins[0]) * (maxs[1] - mins[1])); // min bounding box area.
		const double occupiedPercent = (bbArea / cast(double)area); // % of inventory area used. 0 to 1
		const double bbWastedPrecent = (gaps / bbArea);             // % of bb area wasted with gaps. 0 to 1

		// Position bias. The closer to 0,0 the higher the value. 0 to 1
		const double posBias[2] = [1.0 - (mins[0] / cast(double)width), 1.0 - (mins[1] / cast(double)height)];

		// Final rate: 0 = very poorly organized / 1 = perfect item placement.
		// (Actually we would only get 1 for an empty inventory. If an item is there,
		// it will be less than one, since the used inventory area is taken into account).
		double rate = 1.0 - ((occupiedPercent + bbWastedPrecent) / 2.0);
		rate *= ((posBias[0] + posBias[1]) / 2.0) + 0.1;
		if (rate > 1.0)
		{
			rate = 1.0;
		}

		// A misplaced item gives a 50% rate loss penalty
		if (HasMisplacedItems())
		{
			rate *= 0.5; //FIXME: This is lame!
		}

		/*
		// Some debug printing:
		debug writeln("bb mins.....: ", mins);
		debug writeln("bb maxs.....: ", maxs);
		debug writeln("bb area.....: ", bbArea);
		debug writeln("num gaps....: ", gaps);
		debug writeln("% inventory.: ", occupiedPercent);
		debug writeln("% bb wasted.: ", bbWastedPrecent);
		debug writeln("pos bias....: ", posBias);
		debug writeln("rate........: ", rate);
		*/

		return (rate);
	}

	// Draw inventory and placed items:
	void DrawInventory(int pos = 0) // pos=0 -> left / pos=1 -> right (GL mode only)
	{
		if (noGL)
		{
			for (int x = 0; x < width + 2; ++x) // Top cap
			{
				if ((x == 0) || (x == (width + 1))) { write("+"); } else { write("-"); }
			}
			writeln();

			for (int y = 0; y < height; ++y)
			{
				write("|");
				for (int x = 0; x < width; ++x)
				{
					if (IsOccupied(x, y))
					{
						ItemAt(x, y).DrawItem();
					}
					else
					{
						write(" ");
					}
				}
				writeln("|");
			}

			for (int x = 0; x < width + 2; ++x) // Bottom cap
			{
				if ((x == 0) || (x == (width + 1))) { write("+"); } else { write("-"); }
			}
			writeln();

			int itemCount = 0;
			for (int i = 0; i < items.length; ++i)
			{
				if (items[i].IsValid())
				{
					++itemCount;
				}
			}

			writeln("Item count: ", itemCount);
		}
		else
		{
			// Do this the lazy way:
			// Draw the background and then the items on top of it:

			if (pos == 0)
			{
				drawOffset = 0;
			}
			else
			{
				drawOffset = (this.width * PixelsPerInventorySlot) + 15;
			}

			for (int y = 0; y < height; ++y)
			{
				for (int x = 0; x < width; ++x)
				{
					DrawEmptySlot(x, y);
				}
			}

			foreach (ref Item i; items)
			{
				if (i.IsValid())
				{
					i.DrawItem();
				}
			}
		}
	}
}

// Builds an inventory with a set of items in it. Items are randomly placed, without overlapping.
Inventory GenerateRandomInventory(int inventoryWidth, int inventoryHeight, int numItems, int maxItemSize)
{
	Inventory inventory = new Inventory(inventoryWidth, inventoryHeight, numItems);
	int placementFailures = 0;
	int itemsPlaced = 0;

	while (itemsPlaced < numItems)
	{
		// Get random item rectangle:
		int w, h, x, y;
		RandomItemSize(w, h, maxItemSize);
		x = uniform(0, (inventoryWidth  + 1) - w);
		y = uniform(0, (inventoryHeight + 1) - h);

		// Keep trying to place the item until a free slot is found:
		if (!inventory.IsOccupied(x, y, w, h))
		{
			Item item = new Item(x, y, w, h, RandomItemSymbol(w, h));
			const bool result = inventory.PlaceItem(item);
			assert(result == true);

			placementFailures = 0;
			++itemsPlaced;
		}
		else
		{
			++placementFailures;
		}

		if (placementFailures >= inventory.area)
		{
			// Avoid an infinite loop
			writeln("WARNING: Max attempts to place an item reached! Stopping...");
			break;
		}
	}

	return (inventory);
}
